package com.yst.webplugin.api;

import com.yst.spring.context.SpringContext;
import com.yst.web.utils.ClassUtils;
import com.yst.webplugin.annotation.Default;
import com.yst.webplugin.annotation.RequestParamClass;
import com.yst.webplugin.component.ControllerUtil;
import com.yst.webplugin.valid.All;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Api document builder
 * supports annotation of ApiDocument on high level
 * then supports custom annotation
 */
@Data
@Accessors(chain = true)
public class ApiDocBuilder {

    @Getter
    private final ApiApplication apiApplication;
    private final ApplicationContext context;
    public ApiDocBuilder(ApplicationContext context){
        this.context=context;
        apiApplication =new ApiApplication();
    }


    private final List<Class<? extends Annotation>> requireAnnotationList=new ArrayList<>();

    /**
     * append require annotation of
     * @param classes class of require annotation
     * @return this
     */
    @SafeVarargs
    public final ApiDocBuilder addRequireAnnotation(Class<? extends Annotation>... classes)
    {
        for(Class<? extends Annotation> annotationClass:classes){
            if (!requireAnnotationList.contains(annotationClass)){
                requireAnnotationList.add(annotationClass);
            }
        }
        return this;
    }

    private Boolean existsHasRequireAnnotation(Field field){
        Annotation[] annotations= field.getAnnotations();
        for (Annotation annotation:annotations){
            if (requireAnnotationList.contains(annotation)){
                return true;
            }
        }
        return false;
    }


    private ApiField makeDocField(Field field){

        ApiField docField=new ApiField();
        docField.setFieldName(field.getName());
        docField.setFieldType(field.getType().getName());
        Api.ApiField apiField=field.getAnnotation(Api.ApiField.class);
        if (Objects.nonNull(apiField)){
            LinkedList<Class<?>> genericClassList=(LinkedList<Class<?>>) ClassUtils.getFieldGenericTypeChain(field);
            if (!genericClassList.isEmpty()){
                genericClassList.removeFirst();
            }
            docField.getGenericClassChain().addAll(genericClassList.stream().map(Class::getName).collect(Collectors.toList()));
            docField.setDescription(apiField.value());
            docField.setValueExample(apiField.valueExample());
            List<String> requires=Arrays.stream(apiField.requireGroupClass()).map(Class::getName).collect(Collectors.toList());
            if (!requires.isEmpty()) {
                docField.setRequireClassGroup(Arrays.stream(apiField.requireGroupClass())
                        .map(Class::getName)
                        .collect(Collectors.toList()));
            }

            genericClassList.forEach(this::makeDocEntity);

            return docField;
        }




        Map<String,ApiBuilderConfig> builderConfigs=
                SpringContext.getContext().getBeansOfType(ApiBuilderConfig.class);
        List<EntityConfig> configList=new ArrayList<>();
        if (!builderConfigs.isEmpty()){
            for (ApiBuilderConfig config:builderConfigs.values()){
                config.config(configList);
            }
        }

        if (!configList.isEmpty()){
            for (EntityConfig entityConfig:configList){
                if (Objects.nonNull(entityConfig.getFieldAnnotation())) {
                    Annotation annotation = field.getAnnotation(entityConfig.getFieldAnnotation());
                    try {
                        Method method = annotation.annotationType().getMethod(entityConfig.getFieldCommentFieldName());
                        docField.setDescription((String) method.invoke(annotation));
                    } catch (Exception ignore) {}

                    try {
                        Method method = annotation.annotationType().getMethod(entityConfig.getFieldRequireGroupFieldName());
                        Class<?>[] classList= (Class<?>[]) method.invoke(annotation);
                        docField.setRequireClassGroup(Arrays.stream(classList).map(Class::getName).collect(Collectors.toList()));
                    } catch (Exception ignore) {}

                    return docField;
                }
            }
        }

        return null;
    }

    private ApiEntity fetchEntityComment(Class<?> entityClass){
        Api.ApiEntity apiEntity=entityClass.getAnnotation(Api.ApiEntity.class);
        ApiEntity docEntity=null;
        if (Objects.nonNull(apiEntity)) {
             docEntity = new ApiEntity()
                    .setFieldList(new ArrayList<>())
                    .setClassName(entityClass.getName());
        }
        else {
            Map<String,ApiBuilderConfig> builderConfigs=
                    SpringContext.getContext().getBeansOfType(ApiBuilderConfig.class);

            List<EntityConfig> configList=new ArrayList<>();
            if (!builderConfigs.isEmpty()){
                for (ApiBuilderConfig config:builderConfigs.values()){
                    config.config(configList);
                }
            }

            if (!configList.isEmpty()){
                for (EntityConfig entityConfig:configList){
                    if (Objects.nonNull(entityConfig.getEntityAnnotation())) {
                        Annotation annotation = entityClass.getAnnotation(entityConfig.getEntityAnnotation());
                        try {
                            Method method = annotation.annotationType().getMethod(entityConfig.getEntityCommentFieldName());
                            docEntity = new ApiEntity()
                                    .setFieldList(new ArrayList<>())
                                    .setDescription((String) method.invoke(annotation))
                                    .setClassName(entityClass.getName());
                            break;
                        } catch (Exception ignore) {}
                    }
                }
            }
        }
        if (Objects.nonNull(docEntity)) {
            docEntity.setGeneric(ClassUtils.getGenericClass(entityClass).stream()
                    .map(Class::getName).collect(Collectors.toList()));
        }
        return docEntity;
    }

    private ApiEntity makeDocEntity(Class<?> entityClass){

        if (entityClass==String.class){
            return null;
        }

        if (apiApplication.getEntities().containsKey(entityClass.getName())){
            return apiApplication.getEntities().get(entityClass.getName());
        }

        ApiEntity docEntity=fetchEntityComment(entityClass);
        if (Objects.nonNull(docEntity)) {
            List<String> genericList=ClassUtils.getGenericClass(entityClass).stream().map(Class::getName).collect(Collectors.toList());
            apiApplication.getEntities().put(entityClass.getName(), docEntity);
            Map<String, Field> fieldMap = ClassUtils.getClassFields(entityClass);
            assert fieldMap != null;
            fieldMap.values().forEach((field) -> {
                ApiField docField = makeDocField(field);
                if (Objects.nonNull(docField)) {
                    docEntity.getFieldList().add(docField);
                }
            });
        }
        return docEntity;

    }

    private void fetchMethodParameter(Annotation annotation, ApiMethod method, Api.ApiMethod apiMethod){

        Class<?> validClass=null;


        RequestParamClass requestParamClass=annotation.getClass().getAnnotation(RequestParamClass.class);
        if (Objects.nonNull(requestParamClass)){
            validClass=requestParamClass.paramClass();
        }
        else {
            try {
                validClass = (Class<?>) annotation.annotationType().getMethod("validClass").invoke(annotation);
            }
            catch (Exception e){
                //ignore exception
            }
        }

        if (Objects.isNull(validClass)||validClass== Default.class){
            try {
                validClass = (Class<?>) annotation.annotationType().getMethod("modelClass").invoke(annotation);
            }
            catch (Exception e){
                //ignore exception
            }
        }

        if (Objects.nonNull(validClass)){
            MethodParameter methodParameter=new MethodParameter()
                    .setRequire(true)
                    .setDescription(validClass.getName())
                    .setType(validClass.getName())
                    .setIsPrimitive(ClassUtils.isPrimitive(validClass));
            method.setParameters(methodParameter);
            ApiEntity apiEntity= makeDocEntity(validClass);
            if (Objects.nonNull(apiEntity)){
                getApiApplication().getEntities().put(apiEntity.getClassName(),apiEntity);
            }
        }
    }

    private ApiMethod generateFieldAsMethodDoc(Api api,Field field){
        Api.ApiMethod apiMethod=field.getAnnotation(Api.ApiMethod.class);

        if (Objects.nonNull(apiMethod)){

            String resultClassName=field.getType().getName();
            if (Objects.nonNull(api.resultWrapperClass()) && api.resultWrapperClass() != Default.class) {
                resultClassName=api.resultWrapperClass().getName();
            }

            ApiMethod docMethod=new ApiMethod();
            docMethod.setMethodName(field.getName())
                    .setDescription(apiMethod.value())
                    .setRequestMethod(Arrays.stream(apiMethod.methodType()).map(Enum::name).collect(Collectors.joining("|")))
                    .setPath("/".concat(field.getName()))
                    .setResultType(resultClassName)
                    .setParameters(new ArrayList<>())
                    .setPostDataType(apiMethod.postDataType().name())
                    .setSupportsVersion(apiMethod.versionSince());

            if (StringUtils.isBlank(docMethod.getRequestMethod())){
                docMethod.setRequestMethod(Api.MethodType.POST.name());
            }


            Annotation[] annotations=field.getAnnotations();
            //If there is a name attribute
            for (Annotation annotation: annotations){
                fetchMethodParameter(annotation,docMethod, apiMethod);
            }

            Class<?> paramsClass=apiMethod.paramClass();
            List<Class<?>> allParamClasses = new ArrayList<>(Arrays.asList(paramsClass));
            for (Class<?> param : allParamClasses) {
                if (param!=Default.class) {
                    MethodParameter methodParameter = new MethodParameter()
                            .setName(param.getName())
                            .setDescription("request body")
                            .setType(param.getName())
                            .setIsPrimitive(ClassUtils.isPrimitive(param))
                            .setRequire(false);
                    docMethod.setParameters(methodParameter);
                    makeDocEntity(param);
                }
            }


            List<Class<?>> reClass= ClassUtils.getFieldGenericTypeChain(field);
            if(!reClass.isEmpty()) {
                docMethod.setResultGenerics(reClass.stream().map(Class::getName).collect(Collectors.toList()));
            }
            for(Class<?> clazz:reClass){
                makeDocEntity(clazz);
            }

            return docMethod;
        }
        return null;
    }

    private List<String> getPathFromMethod(Method method,GetMapping getMapping,PostMapping postMapping,RequestMapping requestMapping){
        if (Objects.nonNull(getMapping)&&getMapping.value().length>0){
            return Arrays.asList(getMapping.value());
        }

        if (Objects.nonNull(postMapping)&&postMapping.value().length>0){
            return Arrays.asList(postMapping.value());
        }

        if (Objects.nonNull(requestMapping)&&requestMapping.value().length>0){
            return Arrays.asList(requestMapping.value());
        }

        return Collections.singletonList(method.getName());
    }

    private String getRequestMethod(RequestMapping requestMapping,GetMapping getMapping,PostMapping postMapping){
        if (Objects.nonNull(requestMapping)) {
            return Api.MethodType.GET.name().concat("|").concat(Api.MethodType.POST.name());
        }
        if (Objects.nonNull(getMapping)){
            return Api.MethodType.GET.name();
        }

        if (Objects.nonNull(postMapping)){
            return Api.MethodType.POST.name();
        }

        return "unknown";
    }

    /**
     * generate document of method
     * @param method method
     * @return apiMethod
     */
    private ApiMethod generateMethodDoc(Api api,Method method){
        Api.ApiMethod apiMethod= method.getAnnotation(Api.ApiMethod.class);
        if (Objects.nonNull(apiMethod)){

            RequestMapping requestMapping=method.getAnnotation(RequestMapping.class);
            GetMapping getMapping=method.getAnnotation(GetMapping.class);
            PostMapping postMapping=method.getAnnotation(PostMapping.class);

            String resultClassName=method.getReturnType().getName();
            if (Objects.nonNull(api.resultWrapperClass()) && api.resultWrapperClass() != Default.class) {
                resultClassName=api.resultWrapperClass().getName();
            }


            ApiMethod docMethod=new ApiMethod()
                    .setMethodName(method.getName())
                    .setDescription(apiMethod.value())
                    .setSupportsVersion(apiMethod.versionSince())
                    .setParameters(new ArrayList<>())
                    .setPostDataType(apiMethod.postDataType().name())
                    .setResultType(resultClassName);



            docMethod.setRequestMethod(getRequestMethod(requestMapping,getMapping,postMapping));

            List<String> pathList=getPathFromMethod(method,getMapping,postMapping,requestMapping);
            docMethod.setPath(pathList.size()>1?pathList:pathList.get(0));
            docMethod.setParameters(new ArrayList<>());
            Parameter[] parameters = method.getParameters();


            for (Parameter parameter : parameters) {
                RequestBody requestBody = parameter.getAnnotation(RequestBody.class);
                if (Objects.nonNull(requestBody)) {
                    docMethod.setPostDataType(Api.PostDataType.JSON.name())
                            .setRequestMethod(Api.MethodType.POST.name());
                    MethodParameter methodParameter = new MethodParameter()
                            .setName(parameter.getName())
                            .setDescription("request body")
                            .setType(parameter.getType().getName())
                            .setIsPrimitive(ClassUtils.isPrimitive(parameter.getType()))
                            .setRequire(requestBody.required());
                    ((List<Object>)docMethod.getParameters()).add(methodParameter);
                    Class<?> parameterClass=ClassUtils.fromType(parameter.getType());
                    if (Objects.nonNull(parameterClass)&&!ClassUtils.isPrimitive(parameterClass)){
                        ApiEntity apiEntity=makeDocEntity(parameterClass);
                        if (Objects.nonNull(apiEntity)){
                            apiApplication.getEntities().put(parameterClass.getName(),apiEntity);
                        }
                    }
                }

                PathVariable pathVariable = parameter.getAnnotation(PathVariable.class);
                if (Objects.nonNull(pathVariable)) {
                    MethodParameter methodParameter = new MethodParameter()
                            .setName(pathVariable.value())
                            .setType(parameter.getType().getName())
                            .setIsPrimitive(ClassUtils.isPrimitive(parameter.getType()))
                            .setDescription("path parameter")
                            .setRequire(pathVariable.required());
                    ((List<Object>)docMethod.getParameters()).add(methodParameter);
                }

                RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
                if (Objects.nonNull(requestParam)) {
                    MethodParameter methodParameter = new MethodParameter()
                            .setName(Objects.nonNull(requestParam.name())?requestParam.name():parameter.getName())
                            .setType(parameter.getType().getName())
                            .setIsPrimitive(ClassUtils.isPrimitive(parameter.getType()))
                            .setDescription("request parameter")
                            .setRequire(requestParam.required());
                    ((List<Object>)docMethod.getParameters()).add(methodParameter);
                }
            }

            Type typeClass=method.getGenericReturnType();
            List<Class<?>> genericClass=ClassUtils.getTypeGenericParams(typeClass);
            if (!genericClass.isEmpty()) {
                docMethod.setResultGenerics(genericClass.stream().map(Class::getName).collect(Collectors.toList()));
            }

            for (Class<?> clazz:genericClass){
                if (!ClassUtils.isPrimitive(clazz)){
                    ApiEntity apiEntity=makeDocEntity(clazz);
                    if (Objects.nonNull(apiEntity)) {
                        apiApplication.getEntities().put(clazz.getName(),
                                apiEntity);
                    }
                }
            }

            return docMethod;
        }
        return null;
    }

    private List<String> getControllerPathList(Object controller){
        RequestMapping requestMapping=controller.getClass().getAnnotation(RequestMapping.class);
        if (Objects.nonNull(requestMapping)){
            List<String> pathList= Arrays.asList(requestMapping.value());
            if (pathList.isEmpty()){
                pathList=Arrays.asList(requestMapping.path());
            }

            if (pathList.isEmpty()){
                pathList= Collections.singletonList(requestMapping.name());
            }
            for (int i=0;i<pathList.size();i++){
                if (!pathList.get(i).startsWith("/")){
                    pathList.set(i,"/".concat(pathList.get(i)));
                }
            }
            return pathList;
        }
        return Collections.singletonList("/");
    }

    private Document generateDocument(Object object, Api api){
        Document document=new Document();
        document.setDescription(api.value());



        List<String> pathList=getControllerPathList(object);
        document.setPath(pathList.get(0));
        if (pathList.size()>1){
            document.setPath(pathList);
        }

        Map<String, Field> fieldMap= ClassUtils.getClassFields(object.getClass());
        fieldMap.values().forEach((field)->{
           ApiMethod apiMethod = generateFieldAsMethodDoc(api,field);
           if (Objects.nonNull(apiMethod)){
               document.getMethodList().add(apiMethod);
           }
        });

        Method[] methods=object.getClass().getDeclaredMethods();
        for (Method method:methods){
           ApiMethod apiMethod =generateMethodDoc(api,method);
           if (Objects.nonNull(apiMethod)){
               document.getMethodList().add(apiMethod);
           }
        }
        return document;
    }

    @Setter(AccessLevel.PRIVATE)
    private Map<String,Object> documentMap;

    private void fetchConfigEntities(){
        Map<String,ApiBuilderConfig> builderConfigs=
                SpringContext.getContext().getBeansOfType(ApiBuilderConfig.class);

        List<ApiEntity> entities=new ArrayList<>();
        if (!builderConfigs.isEmpty()){
            for (ApiBuilderConfig config:builderConfigs.values()){
                config.registerEntities(entities);
            }
        }

        for (ApiEntity apiEntity:entities){
            apiApplication.getEntities().put(apiEntity.getClassName(),apiEntity);
        }

    }

    private void refreshParams(){
        documentMap= context.getBeansWithAnnotation(Api.class);

        apiApplication.setPath(ControllerUtil.getContextPath(context));

        documentMap.values().forEach((obj)->{
            Api api =obj.getClass().getAnnotation(Api.class);
            apiApplication.getDocuments().add(generateDocument(obj, api));
        });

        fetchConfigEntities();
    }

    /**
     * function of build document
     * @return ApiApplication {@link ApiApplication}
     */
    public ApiApplication build(){
        refreshParams();
        return apiApplication;
    }


}
